CompletableFuture is a class in Java that provides an easy way to write asynchronous and non-blocking code.
Example:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello, World!"); future.thenAccept(System.out::println);
- thenApply(): Transforms the result and returns a new CompletableFuture.
- thenAccept(): Consumes the result without returning a new CompletableFuture.
Example:
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task 2"); CompletableFuture.allOf(future1, future2).join();
Example using exceptionally():
CompletableFuture.supplyAsync(() -> { throw new RuntimeException("Error!"); }) .exceptionally(ex -> "Recovered from: " + ex.getMessage()) .thenAccept(System.out::println);
handle() allows processing both the result and exception in a single callback.
Example:
CompletableFuture.supplyAsync(() -> "Step 1") .thenApply(result -> result + " -> Step 2") .thenApply(result -> result + " -> Step 3") .thenAccept(System.out::println);
Use runAsync():
CompletableFuture.runAsync(() -> System.out.println("Running asynchronously"));
- get(): Throws checked exceptions.
- join(): Throws unchecked exceptions.
Example using thenCombine():
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello"); CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World"); future1.thenCombine(future2, (a, b) -> a + " " + b) .thenAccept(System.out::println);
- supplyAsync(): Returns a result.
- runAsync(): Does not return a result.
Use thenApply():
CompletableFuture.supplyAsync(() -> "Success") .thenApply(result -> result + " processed") .thenAccept(System.out::println);
Use exceptionally():
CompletableFuture.supplyAsync(() -> { throw new RuntimeException("Error!"); }) .exceptionally(ex -> "Recovered from: " + ex.getMessage()) .thenAccept(System.out::println);
Use thenCombine():
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello"); CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World"); future1.thenCombine(future2, (a, b) -> a + " " + b) .thenAccept(System.out::println);
Use applyToEither():
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Fast Task"); CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Slow Task"); future1.applyToEither(future2, result -> "Result: " + result) .thenAccept(System.out::println);
Use thenCompose():
CompletableFuture.supplyAsync(() -> "First Task") .thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " -> Second Task")) .thenAccept(System.out::println);
Use allOf():
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task 2"); CompletableFuture.allOf(future1, future2).join(); System.out.println("All tasks completed");
Use anyOf():
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Fast Task"); CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Slow Task"); CompletableFuture.anyOf(future1, future2) .thenAccept(result -> System.out.println("First completed: " + result));
Use exceptionally():
CompletableFuture.supplyAsync(() -> { throw new RuntimeException("Error!"); }) .exceptionally(ex -> "Default Value") .thenAccept(System.out::println);
Use whenComplete():
CompletableFuture.supplyAsync(() -> { throw new RuntimeException("Error!"); }) .whenComplete((result, ex) -> System.out.println("Cleanup action executed"));
Use handle():
CompletableFuture.supplyAsync(() -> { throw new RuntimeException("Failure!"); }) .handle((result, ex) -> result != null ? result : "Recovered from: " + ex.getMessage()) .thenAccept(System.out::println);
Use thenAccept() or peek:
CompletableFuture.supplyAsync(() -> "Task Completed") .thenAccept(result -> System.out.println("Log: " + result));
Use completeOnTimeout():
CompletableFuture.supplyAsync(() -> { try { Thread.sleep(5000); } catch (InterruptedException e) { } return "Finished"; }).completeOnTimeout("Default Value", 3, TimeUnit.SECONDS) .thenAccept(System.out::println);
Use cancel():
CompletableFuture<String> future = new CompletableFuture<>(); future.cancel(true); System.out.println("Future cancelled: " + future.isCancelled());
Use completedFuture():
CompletableFuture<String> future = CompletableFuture.completedFuture("Instant Result"); future.thenAccept(System.out::println);
Use failedFuture():
CompletableFuture.failedFuture(new RuntimeException("Something went wrong")) .exceptionally(ex -> "Handled: " + ex.getMessage()) .thenAccept(System.out::println);
Use supplyAsync():
public CompletableFuture<Integer> asyncMethod() { return CompletableFuture.supplyAsync(() -> compute()); } private int compute() { return 42; }
Use runAsync():
CompletableFuture.runAsync(() -> System.out.println("Task running"));
Pass an ExecutorService:
ExecutorService executor = Executors.newFixedThreadPool(5); CompletableFuture.supplyAsync(() -> "Using custom executor", executor) .thenAccept(System.out::println);
Use thenCompose():
CompletableFuture.supplyAsync(() -> 10) .thenCompose(num -> CompletableFuture.supplyAsync(() -> "Result: " + num)) .thenAccept(System.out::println);
Use thenCombine() or thenCombineAsync():
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello"); CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World"); future1.thenCombine(future2, (a, b) -> a + " " + b) .thenAccept(System.out::println);
Use anyOf() and then manually handle timeouts:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); CompletableFuture.anyOf(task1, task2).get(2, TimeUnit.SECONDS);
Use exceptionally() or handle():
CompletableFuture.supplyAsync(() -> { throw new RuntimeException("Error!"); }) .exceptionally(ex -> "Error handled") .thenAccept(System.out::println);
The get() method blocks the thread until the result is available, or an exception is thrown:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello"); String result = future.get(); // Blocks until result is available System.out.println(result);
Use thenApply() or thenCompose():
CompletableFuture.supplyAsync(() -> "Task 1") .thenApply(result -> result + " -> Task 2") .thenApply(result -> result + " -> Task 3") .thenAccept(System.out::println);
Use allOf():
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task 2"); CompletableFuture.allOf(future1, future2).join(); System.out.println("All tasks completed");
thenApply() applies a function to the result, returning a new result.
thenCompose() applies a function returning another CompletableFuture and flattens it:
CompletableFuture.supplyAsync(() -> "Task") .thenApply(result -> result + " Completed") .thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " and Finished")) .thenAccept(System.out::println);
Use sleep() within a supplyAsync task:
CompletableFuture.supplyAsync(() -> { try { Thread.sleep(3000); } catch (InterruptedException e) { } return "Delayed Result"; }).thenAccept(System.out::println);
Use runAsync():
CompletableFuture.runAsync(() -> { System.out.println("Asynchronous Task without result"); });
Use thenApply() to modify the result:
CompletableFuture.supplyAsync(() -> "Initial Result") .thenApply(result -> result + " -> Transformed") .thenAccept(System.out::println);
Use thenCompose() to flatten nested CompletableFutures:
CompletableFuture.supplyAsync(() -> "Task 1") .thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " -> Task 2")) .thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " -> Task 3")) .thenAccept(System.out::println);
Use cancel() on the CompletableFuture instance:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(5000); } catch (InterruptedException e) { } return "Completed"; }); future.cancel(true); // Cancels the task System.out.println("Cancelled: " + future.isCancelled());
Use allOf() or anyOf() to execute tasks and collect results:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); CompletableFuture.allOf(task1, task2).join(); task1.thenAccept(result -> System.out.println("Result: " + result)); task2.thenAccept(result -> System.out.println("Result: " + result));
Use anyOf() to execute an action when any task completes:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); CompletableFuture.anyOf(task1, task2) .thenAccept(result -> System.out.println("Completed: " + result));
Use thenCompose() to chain dependent tasks:
CompletableFuture.supplyAsync(() -> "Task 1") .thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " -> Task 2")) .thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " -> Task 3")) .thenAccept(System.out::println);
Use supplyAsync() to convert:
public CompletableFuture<String> asyncMethod() { return CompletableFuture.supplyAsync(() -> syncMethod()); } public String syncMethod() { return "Synchronous Method Result"; }
Use exceptionally() or handle() to provide a fallback:
CompletableFuture.supplyAsync(() -> { try { Thread.sleep(3000); } catch (InterruptedException e) { } return "Result"; }) .completeOnTimeout("Timeout Occurred", 2, TimeUnit.SECONDS) .thenAccept(System.out::println);
Use thenCombine() to combine results:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); task1.thenCombine(task2, (result1, result2) -> result1 + " " + result2) .thenAccept(System.out::println);
Use runAsync() to execute a task without expecting a result:
CompletableFuture.runAsync(() -> System.out.println("Task running asynchronously"));
Use anyOf() to get the first completed result:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); CompletableFuture.anyOf(task1, task2) .thenAccept(result -> System.out.println("First completed: " + result));
Use handle() or exceptionally() to handle exceptions and provide a fallback:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { if (true) throw new RuntimeException("Error"); return "Success"; }); future.handle((result, ex) -> { if (ex != null) { return "Fallback due to error: " + ex.getMessage(); } return result; }).thenAccept(System.out::println);
Use thenCompose() for task chaining:
CompletableFuture.supplyAsync(() -> "Task 1") .thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " -> Task 2")) .thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " -> Task 3")) .thenAccept(System.out::println);
Use thenCombine() to combine two tasks:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); task1.thenCombine(task2, (result1, result2) -> result1 + " " + result2) .thenAccept(System.out::println);
Use whenComplete() to execute a task after the completion of another:
CompletableFuture.supplyAsync(() -> "Task 1") .whenComplete((result, ex) -> { if (ex != null) { System.out.println("Error occurred: " + ex.getMessage()); } else { System.out.println("Completed: " + result); } }) .thenAccept(System.out::println);
Use supplyAsync() or runAsync() to execute tasks asynchronously:
CompletableFuture.supplyAsync(() -> "Asynchronous Task"); CompletableFuture.runAsync(() -> System.out.println("Running asynchronously"));
Use join() or get() to block the current thread:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Task completed"); String result = future.join(); // Blocks until completion System.out.println(result);
Use CompletableFuture.allOf() to wait for all tasks:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); CompletableFuture.allOf(task1, task2).join(); task1.thenAccept(result -> System.out.println("Result: " + result)); task2.thenAccept(result -> System.out.println("Result: " + result));
Use thenRun() to execute a task after another one completes:
CompletableFuture.supplyAsync(() -> "Task 1") .thenRun(() -> System.out.println("Task 1 finished, executing Task 2"));
Use completeOnTimeout() to set a timeout for a task:
CompletableFuture.supplyAsync(() -> { try { Thread.sleep(5000); } catch (InterruptedException e) { } return "Completed"; }) .completeOnTimeout("Timeout Occurred", 2, TimeUnit.SECONDS) .thenAccept(System.out::println);
Use anyOf() to get the first completed result:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); CompletableFuture.anyOf(task1, task2) .thenAccept(result -> System.out.println("First completed: " + result));
Use thenCombine() with a set of futures to perform an action once all are completed:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); task1.thenCombine(task2, (result1, result2) -> result1 + " and " + result2) .thenAccept(System.out::println);
Use thenCombine() when you need to combine two futures of the same type:
CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> 1); CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> 2); task1.thenCombine(task2, Integer::sum) .thenAccept(result -> System.out.println("Sum: " + result));
Use thenApply() to apply transformations:
CompletableFuture.supplyAsync(() -> "Task 1") .thenApply(result -> result + " transformed") .thenApply(result -> result + " again") .thenAccept(System.out::println);
Use completeOnTimeout() with a custom exception:
CompletableFuture.supplyAsync(() -> { try { Thread.sleep(5000); } catch (InterruptedException e) { } return "Task completed"; }) .completeOnTimeout("Timeout Exception", 2, TimeUnit.SECONDS) .thenAccept(System.out::println);
Use thenRun() to execute after multiple futures:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); CompletableFuture.allOf(task1, task2) .thenRun(() -> System.out.println("All tasks completed"));
Use CompletableFuture.anyOf() to execute on any task completion:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); CompletableFuture.anyOf(task1, task2) .thenAccept(result -> System.out.println("First completed: " + result));
Use completedFuture() to create a future from a value:
CompletableFuture<String> future = CompletableFuture.completedFuture("Already Completed"); future.thenAccept(System.out::println);
Use exceptionally() for exception handling in combined futures:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); CompletableFuture.allOf(task1, task2) .exceptionally(ex -> { System.out.println("Exception: " + ex.getMessage()); return null; });
Use multiple supplyAsync() calls to execute tasks concurrently:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); task1.thenAccept(System.out::println); task2.thenAccept(System.out::println);
Use thenApply() to apply a function:
CompletableFuture.supplyAsync(() -> "Task completed") .thenApply(result -> result + " and transformed") .thenAccept(System.out::println);
Use anyOf() to get the first successful result:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Result from Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Result from Task 2"); CompletableFuture.anyOf(task1, task2) .thenAccept(result -> System.out.println("First completed: " + result));
Use thenCompose() to handle futures sequentially:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); task1.thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " -> Task 2")) .thenAccept(System.out::println);
Use the Executor
parameter in supplyAsync() to run a task in a different thread pool:
Executor executor = Executors.newFixedThreadPool(2); CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> "Task executed in custom thread pool", executor); task.thenAccept(System.out::println);
Use thenAccept() to perform an action without changing the result:
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> "Task completed"); task.thenAccept(result -> System.out.println("Processed: " + result));
Use exceptionally() to handle exceptions:
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> { throw new RuntimeException("Something went wrong"); }); task.exceptionally(ex -> "Default value due to exception: " + ex.getMessage()) .thenAccept(System.out::println);
Use get() to block and retrieve the result:
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> "Task result"); String result = task.get(); System.out.println(result);
Use completeOnTimeout() to schedule a delayed execution:
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> "Delayed task"); task.completeOnTimeout("Timeout result", 2, TimeUnit.SECONDS) .thenAccept(System.out::println);
Use delayedExecutor() to execute a task after a delay:
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> "Delayed task", executor); task.completeOnTimeout("Completed after timeout", 2, TimeUnit.SECONDS) .thenAccept(System.out::println);
Use thenCompose() to chain tasks sequentially, ensuring one task starts after the previous one finishes:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "First task"); task1.thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " -> Second task")) .thenAccept(System.out::println);
Use whenComplete() to run after completion:
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> "Completed task"); task.whenComplete((result, ex) -> { if (ex != null) { System.out.println("Error: " + ex.getMessage()); } else { System.out.println("Result: " + result); } });
Use thenCombine() to run two tasks in parallel and combine their results:
CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> 10); CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> 20); task1.thenCombine(task2, (result1, result2) -> result1 + result2) .thenAccept(System.out::println);
Use thenRun() to perform a task after the CompletableFuture completes:
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> "Task completed"); task.thenRun(() -> System.out.println("Post-task action executed"));
Use completedFuture() to create a CompletableFuture with a predefined result:
CompletableFuture<String> task = CompletableFuture.completedFuture("Constant Value"); task.thenAccept(System.out::println);
Use allOf() to wait for all tasks to finish:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); CompletableFuture<Void> allTasks = CompletableFuture.allOf(task1, task2); allTasks.thenRun(() -> System.out.println("All tasks completed"));
Use thenCompose() to run a task asynchronously after another task completes:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1 completed"); task1.thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " -> Task 2")) .thenAccept(System.out::println);
Use get() with a timeout parameter:
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> "Completed after delay"); String result = task.get(2, TimeUnit.SECONDS); System.out.println(result);
Use cancel() to cancel a CompletableFuture:
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(5000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "Completed"; }); task.cancel(true); System.out.println("Task canceled: " + task.isCancelled());
Use thenApply() to transform the result of an asynchronous task:
CompletableFuture<Integer> task = CompletableFuture.supplyAsync(() -> 10); task.thenApply(result -> result * 2) .thenAccept(System.out::println);
Use handle() to process results or handle exceptions:
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> { if (true) throw new RuntimeException("Error!"); return "Completed successfully"; }); task.handle((result, ex) -> { if (ex != null) { System.out.println("Error: " + ex.getMessage()); } else { System.out.println("Result: " + result); } });
Use a combination of thenApply() and thenAccept() to create a chain of operations:
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> "Task 1"); task.thenApply(result -> result + " -> Task 2") .thenApply(result -> result + " -> Task 3") .thenAccept(System.out::println);
Use thenCombine() to handle multiple independent tasks:
CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> 1); CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> 2); task1.thenCombine(task2, (result1, result2) -> result1 + result2) .thenAccept(System.out::println);
Use thenAccept() to run a callback after a task finishes:
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> "Task Result"); task.thenAccept(result -> System.out.println("Callback executed with: " + result));
Use delay with sleep() inside a CompletableFuture:
CompletableFuture<String> delayedTask = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "Delayed Task Result"; }); delayedTask.thenAccept(System.out::println);
Use thenCompose() to chain tasks sequentially:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Start"); task1.thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " -> Next")) .thenAccept(System.out::println);
Use allOf() to handle multiple independent tasks:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); CompletableFuture<Void> allTasks = CompletableFuture.allOf(task1, task2); allTasks.thenRun(() -> System.out.println("Both tasks are completed"));
Use supplyAsync() to run tasks asynchronously and return a result:
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> "Task completed"); task.thenAccept(result -> System.out.println("Result: " + result));
Use exceptionally() to handle errors in the chain:
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> { if (true) throw new RuntimeException("Error occurred"); return "Task Result"; }); task.exceptionally(ex -> "Error handled: " + ex.getMessage()) .thenAccept(System.out::println);
Use thenCombine() for combining futures:
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 100); CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 200); future1.thenCombine(future2, (result1, result2) -> result1 + result2) .thenAccept(System.out::println);
Use runAsync() when you don’t need a result:
CompletableFuture.runAsync(() -> System.out.println("Task completed without a result"));
Use thenApply() to handle successful results:
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1"); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2"); task1.thenCombine(task2, (result1, result2) -> result1 + " & " + result2) .thenApply(result -> "Combined Result: " + result) .thenAccept(System.out::println);